#!/usr/bin/python

import gnome
gnomeProgram = gnome.init("StageWhisper", "0.1")
import pygtk
pygtk.require('2.0')

import gtk, gtk.glade, gtk.gdk

from twisted.internet import gtk2reactor
gtk2reactor.install()

from twisted.internet import reactor, protocol, defer, task
import alsaaudio, time, gettext, sys, os, os.path

import gst

MIXER = "PCM"
MIXER = alsaaudio.Mixer(MIXER)

def setVolume(vol=100):
    MIXER.setvolume(int(vol), alsaaudio.MIXER_CHANNEL_ALL)

class _Fader:
    def __init__(self, speed):
        self.level = MIXER.getvolume()[0]
        self.speed = speed
        self.delta = self.level / 20.0
        self.d = defer.Deferred()
        reactor.callLater(speed, self.fade)

    def fade(self):
        self.level -= self.delta
        setVolume(max(self.level,0))
        if self.level <= 0.1:
            d, self.d = self.d, None
            d.callback(True)
        else:
            reactor.callLater(self.speed, self.fade)

def fadeVolume(speed):
    f = _Fader(speed)
    return f.d

def loadSoundFiles(filename):
    for num,line in enumerate(open(filename)):
        if not line.strip(): 
            continue
        snd,name = line.strip().split(':')
        if not os.path.exists(snd):
            print "ERROR: file %s at %s:%d does not exist!"%(snd,filename,num+1)
        col = num // 5
        row = num % 5
        yield (num, row, col, snd, name)




class ProcessOutput(protocol.ProcessProtocol):
    master = pid = None

    def __init__(self, output=sys.stdout):
        self.output = output
        self.defer = defer.Deferred()

    def connectionMade(self):
        self.output.write("[process spawned]\n")

    def outReceived(self, data):
        self.output.write(data)

    def errReceived(self, data):
        self.output.write(data)

    def processEnded(self, status_object):
        exitval = status_object.value.exitCode
        self.output.write("[process ended with exitcode %s]\n"%(exitval))
        self.triggerDeferred(exitval)
        self.master._donePlaying(self.pid)

    def triggerDeferred(self, exitval):
        self.defer.callback(exitval)
        self.defer = None

class ExternalPlayer:
    playerName = 'external'
    def _initPlayer(self):
        self.pids = []
        self.pidDict = {}

    def killAllSound(self, pids=None):
        import os
        if pids is None:
            print "no pids specified, using", self.pids
            pids = self.pids
        else:
            print "killing", pids
        for p in pids:
            try:
                os.kill(p, 9)
            except: 
                print "failed to kill PID %d - already quit?"%(p)
        self.pids = []
        #time.sleep(0.2)

    def _donePlaying(self, pid):
        sndFile = self.pidDict.get(pid)
        button = self.buttonDict.get(sndFile)
        # XXX doesn't work?
        new_style = button.get_style().copy()
        # change the style attributes
        new_style.bg[gtk.STATE_NORMAL] = gtk.gdk.Color(20000,65535,20000)
        # fill out the new style by attaching it to the widget
        button.set_style(new_style)
        if pid in self.pidDict:
            del self.pidDict[pid]
        if pid in self.pids:
            self.pids.remove(pid)
        # XXX reset the Now Playing widget
        self.updateNowPlaying()

    def play(self, w, e, snd):
        if e.type != gtk.gdk.BUTTON_PRESS:
            return
        if e.button == 1:
            self._playSound(snd)
        elif e.button == 3:
            vol = MIXER.getvolume()[0]
            d = fadeVolume(0.02)
            d.addCallback(lambda x, p=self.pids: self.killAllSound(p))
            d.addCallback(lambda x: reactor.callLater(0.3, setVolume, vol))
            d.addCallback(lambda x,snd=snd: 
                                    self._playSound(snd))

    def _playSound(self, soundfile):
        pp = ProcessOutput()
        pp.master = self
        if soundfile.lower().endswith('.mp3'):
            cmdargs = [ "/usr/bin/mpg123", "-q", "-o", "alsa", soundfile ]
        else:
            cmdargs = [ "/usr/bin/aplay", "-Dplug:dmix", soundfile ]
        proc = reactor.spawnProcess(pp, cmdargs[0], cmdargs, {})
        self.pids.append(proc.pid)
        self.pidDict[proc.pid] = os.path.basename(soundfile)
        pp.pid = proc.pid
        self.updateNowPlaying()
        return proc.pid

class GstreamerPlayer:
    playerName = 'gstreamer'
    def _initPlayer(self):
        self.playingSources = {}

    def play(self, w, e, snd):
        if e.type != gtk.gdk.BUTTON_PRESS:
            return
        if e.button == 1:
            # play new sound
            pass
        elif e.button == 3:
            # fade existing, then play
            pass

    def _playSound(self, filename):
        bin = gst.Thread('player%s'%filename)
        outsink = gst.element_factory_make('alsasink', 'sink')
        outsink.set_property('device', 'plug:dmix')
        source = gst.element_factory_make('filesrc', 'src')
        source.set_property('location', filename)
        volume = gst.element_factory_make('volume', 'volume')
        volume.set_property('volume', 1.0)
        bin.add_many(source, volume, outsink)
        gst.element_link_many(source, volume, outsink)
        self.playingSources[filename] = bin
        bin.set_state(gst.STATE_PLAYING)







class StageWhisperUI:
    # Mixin a player class to make a working application
    def __init__(self, files):
        self.buttonDict = {}
        domain = gettext.textdomain()
        gtk.glade.textdomain(domain)
        self.xml = gtk.glade.XML("stagewhisper.glade", None, 
                                                    gettext.textdomain())
        self.xml.signal_autoconnect(self)
        win = self.xml.get_widget('StageWhisperMainWindow')
        win.connect("destroy", self.closeApplication)
        win.connect("delete_event", self.closeApplication)

        appbar = self.xml.get_widget('appbar1') 
        #appbar.set_status('using %s'%(self.playerName))
        table = self.xml.get_widget('SoundTable') 
        allsnds = loadSoundFiles(files)
        for num,row,col,snd,name in allsnds:
            # Save the sounds list off for the PID:sound mapping
            button = gtk.Button("%02d %s"%(num, name,))
            button.set_events(gtk.gdk.BUTTON_PRESS_MASK)
            button.connect("event",  lambda w,e,snd=snd: self.play(w,e,snd))
            table.attach(button,row,row+1,col,col+1)
            button.show()
            self.buttonDict[os.path.basename(snd)] = button
        win.show_all()
        self.startVolumeChecking()
        #self._initPlayers()

    def closeApplication(self, *ignored):
        # XXX check whether anything is playing
        reactor.stop()

    def startVolumeChecking(self):
        self.vscale = self.xml.get_widget('volumeScale') 
        lc = task.LoopingCall(self._volumeCheck)
        lc.start(0.2)
        
    def _volumeCheck(self):
        volumes = MIXER.getvolume()
        self.vscale.set_value(volumes[0])
        #print volumes, self.vscale.get_value()

    def on_volumeScale_change_value(self, w, scroll, vol):
        if vol < 0: vol = 0.0
        if vol > 100: vol = 100.0
        setVolume(vol)

    def on_quit1_activate(self, *e):
        reactor.stop()

    def on_muteButton_toggled(self, *e):
        # XXX implement this!
        print "MUTE", e

    def updateNowPlaying(self):
        items = self.pidDict.items()
        items.sort()
        out = '\n'.join(x[1] for x in items)
        nowplaying = self.xml.get_widget('nowplaying')
        nowplaying.get_buffer().set_text(out)

    def on_fadeButtonQuick_clicked(self, *e):
        vol = MIXER.getvolume()[0]
        d = fadeVolume(0.02)
        d.addCallback(lambda x, p=self.pids: self.killAllSound(p))
        d.addCallback(lambda x: reactor.callLater(0.4, setVolume, vol))
        
    def on_MedFadeButton_clicked(self, *e):
        vol = MIXER.getvolume()[0]
        d = fadeVolume(0.08)
        d.addCallback(lambda x, p=self.pids: self.killAllSound(p))
        d.addCallback(lambda x: reactor.callLater(0.4, setVolume, vol))

    def on_SlowFadeButton_clicked(self, *e):
        vol = MIXER.getvolume()[0]
        d = fadeVolume(0.20)
        d.addCallback(lambda x, p=self.pids: self.killAllSound(p))
        d.addCallback(lambda x: reactor.callLater(0.4, setVolume, vol))

    def on_KillNowButton_clicked(self, *e):
        self.killAllSound()
        self.killAllSound(self.pids)


    def on_new1_activate(self, *e):
        pass
    def on_open1_activate(self, *e):
        pass
    def on_save1_activate(self, *e):
        pass
    def on_save_as1_activate(self, *e):
        pass
    def on_cut1_activate(self, *e):
        pass
    def on_copy1_activate(self, *e):
        pass
    def on_paste1_activate(self, *e):
        pass
    def on_clear1_activate(self, *e):
        pass
    def on_properties1_activate(self, *e):
        pass
    def on_preferences1_activate(self, *e):
        pass
    def on_about1_activate(self, *e):
        pass

class StageWhisperExternal(StageWhisperUI,ExternalPlayer):
    pass
class StageWhisperGstreamer(StageWhisperUI,GstreamerPlayer):
    pass

def main():
    import optparse
    parser = optparse.OptionParser(version='%prog 0.1')
    parser.add_option('-p', '--player', dest='player', type='choice',
                      choices = ('gstreamer','external'),
                      help='use player PLAYER (gstreamer or external (default)',
                      metavar='PLAYER', default='external')
    (opts,args) = parser.parse_args()
    filename = args[0]
    if opts.player == 'gstreamer':
        klass = StageWhisperGstreamer
    else:
        klass = StageWhisperExternal
    win = klass(filename)
    reactor.run()

if __name__ == "__main__":
    main()
